<template>
  <div class="page-container">
    <input ref="openFile" type="file" v-show="false" @change="onOpenFile" accept="application/vnd.openxmlformats-officedocument.presentationml.presentation" />
    <input ref="addFile" type="file" v-show="false" @change="onAddFile" />
    <div class="page-body">
      <div class="page-title" v-ripple>OpenXML 文档内容编辑器</div>
      <div class="page-content">
        <div class="page-left">
          <div class="toolbar">
            <div class="toolbar-item" v-ripple @click="openFile">
              <div class="icon-filetypes-pptx"></div>
            </div>
            <div class="toolbar-item" v-ripple @click="refreshNode">
              <div class="icon-refresh refresh"></div>
            </div>
          </div>
          <div class="file-tree">
            <VueTree
              :data="files"
              text-field-name="name"
              value-field-name="path"
              children-field-name="items"
              whole-row
              @item-click="onFileItemClick">
              </VueTree>
          </div>
        </div>

        <div class="page-right">
          <div class="toolbar">
            <div class="toolbar-item" v-ripple @click="addFile">
              <div class="item-icon icon-add add-file"></div>
            </div>
            <div class="toolbar-item" v-ripple @click="deleteFile">
              <div class="item-icon icon-delete delete-file"></div>
            </div>
            <div class="toolbar-item" v-ripple @click="showTestXmlFile">
              <div class="item-icon icon-test"></div>
            </div>
            <div class="toolbar-item toolbar-text">
              <div class="toolbar-info">[{{currentFileFullPath}}]</div>
            </div>
            <div class="toolbar-item" v-ripple @click="updateZipFile">
              <div class="item-icon icon-zip update-file"></div>
            </div>
            <div class="toolbar-item" v-ripple @click="saveFile">
              <div class="item-icon icon-save"></div>
            </div>
          </div>
          <div class="file-panel">
            <codemirror ref="codeEditor" class="code-mirror" v-model="viewers.code.src" :options="codeMirrorOptions" v-show="viewers.code.enable"></codemirror>
            <div class="image-viewer" v-show="viewers.image.enable">
              <input ref="openImage" type="file" v-show="false" @change="onOpenImage" accept="image/png,image/jpg,image/jpeg,image/gif" />
              <img class="image-item" :src="viewers.image.src" @click="changeImage"/>
            </div>
            <div class="invalid-viewer" v-show="viewers.invalid.enable">
              此文档类型无法编辑或预览！
            </div>
          </div>
        </div>
      </div>
    </div>
    <div v-show="tester.xmlParser.enable" class="xml-test-popup" @click="onXmlParserTesterClick">
      <div class="xml-test-container">
        <div class="test-data">
          <codemirror ref="jsonEditor" class="json-code-mirror" v-model="tester.xmlParser.json" :options="tester.xmlParser.options"></codemirror>
        </div>
        <div class="toolbar">
          <div class="toolbar-item icon-test" @click="doXmlParserTest"></div>
          <div class="toolbar-item icon-return return"></div>
        </div>
        <div class="test-result">
          <textarea v-model="tester.xmlParser.result"></textarea>
        </div>
      </div>
    </div>
    <router-view />
  </div>
</template>
<script>
import { VueTree, OpenXmlXP, sleep } from 'base'
import { codemirror } from 'vue-codemirror-lite'
import XmlBeautify from 'xml-beautify'
import { Indicator, MessageBox } from 'mint-ui'

require('codemirror/mode/xml/xml')
require('codemirror/mode/javascript/javascript')
require('codemirror/addon/hint/show-hint.js')
require('codemirror/addon/hint/show-hint.css')
require('codemirror/addon/hint/xml-hint.js')
require('codemirror/addon/lint/json-lint')

export default {
  components: { VueTree, codemirror },
  data () {
    return {
      files: [],
      codeMirrorOptions: {
        mode: 'xml',
        tabSize: 2,
        lineNumbers: true
      },
      zipFiles: {},
      viewers: {
        code: {
          enable: false,
          src: ''
        },
        image: {
          enable: false,
          src: ''
        },
        invalid: {
          enable: false
        }
      },
      currentItem: {
        node: null,
        item: {
          name: '',
          fullPath: ''
        }
      },
      tester: {
        xmlParser: {
          json: '',
          result: '',
          options: {
            mode: 'javascript',
            tabSize: 2,
            lineNumbers: true
          },
          enable: false
        }
      },
      fileIdentity: 0
    }
  },
  mounted () {
  },
  computed: {
    currentFileFullPath () {
      if (!this.currentItem.node) {
        return '...'
      }
      let item = this.currentItem.item
      let root = item.root
      let names = [item.fullPath || item.path]
      while (root) {
        if (root.fullPath) {
          names.push(root.fullPath)
        } else if (root.name) {
          names.push(root.name)
        }
        root = root.root
      }
      names.reverse()
      return names.join('/')
    }
  },
  methods: {
    resetCurrentItem () {
      this.currentItem = {
        node: null,
        item: {
          name: '',
          fullPath: ''
        }
      }
    },
    async onFileItemClick (node, item, e) {
      this.currentItem = {
        node,
        item
      }
      this.viewers.code.src = ''
      if (item.dir) {
        item.opened = !item.opened
        return
      }
      // 读取文件文本内容
      Object.keys(this.viewers).forEach(key => {
        this.viewers[key].enable = false
      })
      let file = item.root.zipFile.files[item.fullPath]
      switch (item.ext) {
        case '.xml':
        case '.rels':
        case '.xml.rels':
          this.viewers.code.enable = true
          var xmlCode = await file.async('text')
          this.viewers.code.src = new XmlBeautify().beautify(xmlCode, {
            indent: '  ',
            useSelfClosingElement: true
          })
          break
        case '.png':
        case '.gif':
        case '.jpg':
        case '.jpeg':
        case '.tiff':
          this.viewers.image.enable = true
          this.viewers.image.src = await this.createDataUrl(file, item.ext)
          break

        default:
          this.viewers.invalid.enable = true
          break
      }
    },
    async createDataUrl (file, ext) {
      let base64Data = await file.async('base64')
      return `data:image/${ext.replace('.')};base64,${base64Data}`
    },
    addFile () {
      let item = this.currentItem.item
      let node = this.currentItem.node
      if (!node || !item) {
        return
      }
      this.$refs.addFile.click()
    },
    async onAddFile (e) {
      if (!e.target.files || !e.target.files.length) {
        return
      }
      Indicator.open('正在添加文件，请稍后...')
      let file = e.target.files[0]
      let item = this.currentItem.item
      let root = null
      if (item.root) {
        root = item.root
        if (item.root.path === '/') {
          root = item.root.root ? item.root.root : item.root
        }
      } else {
        root = item
      }
      let folder = item.dir ? item : item.folder
      let folderPath = folder.path !== '/' ? folder.path : ''
      let zipFile = root.zipFile
      let fullPath = `${folderPath}${file.name}`
      let fileData = await file.loadBlob()
      zipFile.file(fullPath, fileData, { binary: true })
      let fileItem = OpenXmlXP.getFileInfo(fullPath)
      fileItem.dir = false
      fileItem.root = root
      fileItem.folder = folder
      await this.parseTreeItem(file, zipFile, fileItem)
      while (root) {
        root.root && (root.root.zipFile.file(root.fullPath, root.zipFile, { binary: true }))
        root = root.root
      }
      folder.items.push(fileItem)
      Indicator.close()
    },
    async deleteFile () {
      if (!this.currentItem.node) {
        return
      }
      let item = this.currentItem.item
      try {
        await MessageBox.confirm(`确定要删除 [${item.name}] 吗？`)
      } catch (ex) {
        return
      }
      if (item.path === '/' && !item.root) {
        delete this.zipFiles[item.name]
        return this.removeTreeNode(item)
      }
      Indicator.open('正在删除文件，请稍后...')
      let root = item.root
      let childZipFile = null
      let fullPath = null
      let zipFile = null
      while (root) {
        if (root.fullPath) {
          fullPath = root.fullPath
          root = root.root
          continue
        }
        zipFile = root.zipFile
        if (zipFile) {
          if (!childZipFile) {
            childZipFile = zipFile
            childZipFile.remove(item.name)
          } else {
            let childZipFileData = await childZipFile.generateAsync({
              type: 'blob'
            })
            zipFile.file(fullPath, childZipFileData, { binary: true })
          }
        }
        root = root.root
      }
      this.removeTreeNode(item)
      this.resetCurrentItem()
      await sleep(500)
      Indicator.close()
    },
    removeTreeNode (item) {
      let items = item.root ? (item.folder ? item.folder.items : item.items) : this.files
      if (!items) {
        return -1
      }
      let idx = items.indexOf(item)
      if (idx >= 0) {
        items.splice(idx, 1)
      }
      return idx
    },
    async refreshNode () {
      if (!this.currentItem.node) {
        return
      }
      Indicator.open('正在刷新，请稍后...')
      let item = this.currentItem.item
      let root = item.root || item
      while (root.root) {
        root = root.root
      }
      let {localFile} = this.zipFiles[root.name]
      let {files} = await this.loadFile(localFile)
      let idx = this.removeTreeNode(root)
      if (idx >= 0) {
        this.files.splice(idx, 0, files[0])
      }
      this.viewers.code.src = ''
      this.viewers.image.src = ''
      this.resetCurrentItem()
      await sleep(500)
      Indicator.close()
    },
    openFile () {
      this.$refs.openFile.click()
    },
    async onOpenFile (e) {
      if (!e.target.files || !e.target.files.length) {
        return
      }
      Indicator.open('正在加载文件，请稍后...')
      let localFile = e.target.files[0]
      if (!this.zipFiles[localFile.name]) {
        let {files, zipFile} = await this.loadFile(localFile)
        this.files.push.apply(this.files, files)
        this.zipFiles[localFile.name] = {
          localFile,
          zipFile
        }
      }
      Indicator.close()
    },
    async saveFile () {
      let fileItem = this.files[0]
      if (!fileItem) {
        return
      }
      Indicator.open('正在生成文件，请稍后...')
      let zipFile = fileItem.zipFile
      var zipData = await zipFile.generateAsync({ type: 'blob' })
      window.saveAs(zipData, fileItem.name)
      Indicator.close()
    },
    async updateZipFile () {
      let item = this.currentItem.item
      if (!item || !item.fullPath) {
        return
      }
      let fullPath = null
      let zipFile = null
      let childZipFile = null
      let root = item
      Indicator.open('正在保存文件，请稍后...')
      while (root) {
        if (root.fullPath) {
          fullPath = root.fullPath
          root = root.root
          continue
        }
        zipFile = root.zipFile
        if (zipFile) {
          if (!childZipFile) {
            zipFile.file(fullPath, this.viewers.code.src)
            childZipFile = zipFile
          } else {
            let childZipFileData = await childZipFile.generateAsync({
              type: 'blob'
            })
            zipFile.file(fullPath, childZipFileData, { binary: true })
          }
        }
        root = root.root
      }
      await sleep(1000)
      Indicator.close()
    },
    async parseTreeItem (file, zipFile, item) {
      let icon = ''
      if (item.name !== '/') {
        if (item.name.length > 1 && item.name.indexOf('.') >= 0) {
          let ext = item.name.split('.').pop().toLowerCase()
          switch (ext) {
            case 'gif':
            case 'jpeg':
            case 'jpg':
            case 'bmp':
            case 'tiff':
              ext = 'image'
              break

            case 'xlsx':
              let xlsxFileData = await zipFile.file(item.fullPath).async('blob')
              let xlsxRes = await this.loadFile(xlsxFileData)
              let rootItem = xlsxRes.files[0]
              rootItem.root = item
              item.items = rootItem.items
              item.zipFile = xlsxRes.zipFile
              break

            default:
              ext = 'dat'
              break
          }
          icon = `icon-filetypes-${ext} icon-small`
        }
      } else {
        item.name = file.name
        icon = `icon-filetypes-pptx icon-middle icon-file-root`
      }

      Object.assign(item, {
        id: this.fileIdentity++,
        icon,
        opened: false,
        selected: false,
        disabled: false,
        loading: false
      })
      return item
    },
    async loadFile (localFile) {
      let zipFile = await OpenXmlXP.loadBlob(localFile)
      let files = await OpenXmlXP.getFiles(zipFile, async (item, idx) => {
        await this.parseTreeItem(localFile, zipFile, item)
      })
      return {
        files,
        zipFile
      }
    },
    changeImage () {
      if (!this.currentItem.node) {
        return
      }
      this.$refs.openImage.click()
    },
    async onOpenImage (e) {
      if (!this.currentItem.node) {
        return
      }
      let fileItem = this.currentItem.item
      let imageData = e.target.files[0]
      let data = await imageData.loadBlob()
      let zipFile = fileItem.root.zipFile
      zipFile.file(fileItem.fullPath, data, {binary: true})
      this.onFileItemClick(this.currentItem.node, this.currentItem.item)
    },
    showTestXmlFile () {
      if (!this.currentItem.node) {
        return
      }
      this.tester.xmlParser.enable = true
      this.tester.xmlParser.json = this.getCachedTestData(this.currentItem.item)
    },
    onXmlParserTesterClick (e) {
      let classList = e.srcElement.classList
      this.tester.xmlParser.enable = !classList.contains('xml-test-popup') && !classList.contains('return')
    },
    doXmlParserTest () {
      let jsonData = this.tester.xmlParser.json
      if (!jsonData) {
        return
      }
      let xmlCode = this.viewers.code.src
      if (!xmlCode) {
        return
      }
      let root = this.getItemRoot(this.currentItem.item)
      localStorage.setItem(root.name, jsonData)
      try {
        jsonData = JSON.parse(jsonData)
        // eslint-disable-next-line no-undef
        let result = template.render(xmlCode, jsonData)
        this.tester.xmlParser.result = result
      } catch (ex) {
        console.error(ex.message)
      }
    },
    getItemRoot (item) {
      if (!item.root) {
        return item
      }
      let root = item.root
      while (root.root) {
        root = root.root
      }
      return root
    },
    getCachedTestData (item) {
      let root = this.getItemRoot(item)
      return localStorage.getItem(root.name)
    }
  }
}
</script>
<style lang="scss" scoped>
.page-container {
  width: 100vw;
  height: 100vh;
  color: #333;

  .page-body {
    display: flex;
    flex-flow: column;
    height: 100%;

    .page-title {
      font-size: 2vw;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 15px 0px;
      min-height: 45px;
    }

    .page-content {
      flex-grow: 1;
      display: flex;
      border-top: 1px solid #aaa;
      padding: 15px;

      .page-left {
        display: flex;
        flex-flow: column;
        .toolbar {
          border: solid 1px #aaa;
          height: 38px;
          min-height: 38px;
          margin-bottom: 5px;
          padding: 2px;
          display: flex;
          justify-content: flex-end;
          align-items: center;

          .toolbar-item {
            width: 32px;
            height: 32px;
            border: solid 1px lightgray;
            color: lightskyblue;
            cursor: pointer;
            transition: all ease 0.3s;
            border-radius: 4px;
            display: flex;
            justify-content: center;
            align-items: center;

            &:hover {
              border-color: #aaa;
              color: lightpink;
            }

            & + .toolbar-item {
              margin-left: 5px;
            }

            .refresh {
              font-size: 24px;
            }
          }
        }

        .file-tree {
          flex-grow: 1;
          width: 20vw;
          border: solid 1px #aaa;
          overflow: auto;
        }
      }

      .page-right {
        margin-left: 15px;
        flex-grow: 1;
        display: flex;
        border: solid 1px #aaa;
        flex-flow: column;

        .toolbar {
          border-bottom: solid 1px #aaa;
          height: 38px;
          min-height: 38px;
          margin-bottom: 5px;
          padding: 2px;
          display: flex;
          justify-content: flex-end;
          align-items: center;

          .toolbar-item {
            width: 32px;
            height: 32px;
            border: solid 1px lightgray;
            color: lightskyblue;
            cursor: pointer;
            transition: all ease 0.3s;
            border-radius: 4px;
            display: flex;
            justify-content: center;
            align-items: center;

            .item-icon {
              font-size: 24px;
            }

            .add-file {
              font-size: 22px;
            }

            .update-file {
              font-size: 22px;
            }

            .delete-file {
              font-size: 26px;
            }

            & + .toolbar-item {
              margin-left: 5px;
            }

            &:hover {
              border-color: #aaa;
              color: lightpink;
            }

            &.toolbar-text {
              width: auto;
              padding: 0px 4px;
              flex-grow: 1;
              border: none;
              justify-content: flex-start;
              font-size: 18px;
            }
          }
        }

        .file-panel {

          .code-mirror {
            width: calc(80vw - 45px);
            height: calc(100vh - 156px);
            display: flex;
            flex-flow: column;
          }

          .image-viewer {
            width: calc(80vw - 75px);
            height: calc(100vh - 200px);
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 15px;

            .image-item {
              max-width: 100%;
              max-height: 100%;

              cursor: pointer;

              &:hover {
                box-shadow: 0px 0px 25px #666;
              }
            }
          }

          .invalid-viewer {
            width: 100%;
            height: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
            font-size: 20px;
          }
        }
      }
    }
  }

  .xml-test-popup {
    position: fixed;
    z-index: 9999;
    left: 0px;
    top: 0px;
    right: 0px;
    bottom: 0px;
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: rgba(0, 0, 0, 0.5);
    transition: all ease 500ms;

    .xml-test-container {
      display: flex;
      width: 90vw;

      .test-data {
        border: 1px solid #666;
        width: calc(45vw - 53px);
        .json-code-mirror {

          height: calc(90vh - 4px);
        }
      }

      .toolbar {
        width: 100px;
        display: flex;
        flex-flow: column;
        justify-content: center;
        align-items: center;
        background-color: white;
        border-top: 1px solid #666;
        border-bottom: 1px solid #666;

        .toolbar-item {
          width: 64px;
          height: 64px;
          border: solid 1px lightgray;
          color: lightskyblue;
          cursor: pointer;
          transition: all ease 0.3s;
          border-radius: 4px;
          display: flex;
          justify-content: center;
          align-items: center;
          font-size: 52px;

          &:hover {
            border-color: #aaa;
            color: lightpink;
          }

          & + .toolbar-item {
            margin-top: 10px;
          }

          &.return {
            font-size: 48px;
          }
        }
      }

      .test-result {
        border: 1px solid #666;
        width: calc(45vw - 53px);
        display: flex;

        > textarea {
          border: none;
          flex-grow: 1;
          overflow: scroll;
        }
      }
    }
  }
}
</style>
